<?php
/**
 * 摘自 ThinkPHP 的SQL Builder
 * @author    Mark <zhaody901@126.com>
 * @code    $sqlBuilder->emptyOpts()->table('brand')->where($data)->buildSelect();
 * @version  1.0
 *
 */
class SQLBuilder {

    /**
     * 对字符串类型字段采用模糊匹配
     */
    private $_dbLikeFields = '';

    /**
     * @var string  数据表前缀
     */
    private $_tbPre;

    /**
     * @var 生成表格的选项.
     */
    private $_options;

    // 数据库表达式
    private $_comparison = array('eq'=>'=','neq'=>'<>','gt'=>'>','egt'=>'>=','lt'=>'<','elt'=>'<=','notlike'=>'NOT LIKE','like'=>'LIKE','in'=>'IN','notin'=>'NOT IN');

    // 查询表达式
    private $_selectSql  = 'SELECT%DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%%LIMIT% %UNION%';


    public function __construct($dbPre)
    {
        $this->_tbPre = $dbPre;
    }

    public function setDbLikeFields($fileds)
    {
        $this->_dbLikeFields = $fileds;
    }

    /**
     * set分析
     * @param array $data
     * @return string
     */
    private function parseSet($data) {
        $set = array();
        foreach ($data as $key=>$val){
            $value   =  $this->parseValue($val);
            if(is_scalar($value)) // 过滤非标量数据
                $set[]    = $this->parseKey($key).'='.$value;
        }
        return ' SET '.implode(',',$set);
    }

    /**
     * 字段名分析
     * @access private
     * @param string $key
     * @return string
     */
    private function parseKey(&$key) {
        return $key;
    }

    /**
     * value分析
     * @access private
     * @param mixed $value
     * @return string
     */
    private function parseValue($value) {
        if(is_string($value)) {
            $value =  '\''.$this->escapeString($value).'\'';
        }elseif(isset($value[0]) && is_string($value[0]) && strtolower($value[0]) == 'exp'){
            $value =  $this->escapeString($value[1]);
        }elseif(is_array($value)) {
            $value =  array_map(array($this, 'parseValue'),$value);
        }elseif(is_bool($value)){
            $value =  $value ? '1' : '0';
        }elseif(is_null($value)){
            $value =  'null';
        }
        return $value;
    }

    /**
     * field分析
     * @access private
     * @param mixed $fields
     * @return string
     */
    private function parseField($fields) {
        if(is_string($fields) && strpos($fields,',')) {
            $fields    = explode(',',$fields);
        }
        if(is_array($fields)) {
            // 完善数组方式传字段名的支持
            // 支持 'field1'=>'field2' 这样的字段别名定义
            $array   =  array();
            foreach ($fields as $key=>$field){
                if(!is_numeric($key))
                    $array[] =  $this->parseKey($key).' AS '.$this->parseKey($field);
                else
                    $array[] =  $this->parseKey($field);
            }
            $fieldsStr = implode(',', $array);
        }elseif(is_string($fields) && !empty($fields)) {
            $fieldsStr = $this->parseKey($fields);
        }else{
            $fieldsStr = '*';
        }
        //TODO 如果是查询全部字段，并且是join的方式，那么就把要查的表加个别名，以免字段被覆盖
        return $fieldsStr;
    }

    /**
     * table分析
     * @param $table
     * @return string
     */
    private function parseTable($table) {
        // if has prefix
        if ( substr_compare($this->_tbPre, $table, 0, strlen($this->_tbPre)) == 0 )
            return $table;
        return $this->_tbPre . $this->parseKey($table);
    }

    /**
     * where分析
     * @param mixed $where
     * @return string
     */
    private function parseWhere($where) {
        $whereStr = '';
        if(is_string($where)) {
            // 直接使用字符串条件
            $whereStr = $where;
        }else{ // 使用数组或者对象条件表达式
            if(isset($where['_logic'])) {
                // 定义逻辑运算规则 例如 OR XOR AND NOT
                $operate    =   ' '.strtoupper($where['_logic']).' ';
                unset($where['_logic']);
            }else{
                // 默认进行 AND 运算
                $operate    =   ' AND ';
            }
            foreach ($where as $key=>$val){
                $whereStr .= '( ';
                if(0===strpos($key,'_')) {
                    // 解析特殊条件表达式
                    $whereStr   .= $this->parseThinkWhere($key,$val);
                }else{
                    // 查询字段的安全过滤
                    if(!preg_match('/^[A-Z_\|\&\-.a-z0-9\(\)\,]+$/',trim($key))){
                        $this->_handleError('_EXPRESS_ERROR_:'.$key);
                    }
                    // 多条件支持
                    $multi  = is_array($val) &&  isset($val['_multi']);
                    $key    = trim($key);
                    if(strpos($key,'|')) { // 支持 name|title|nickname 方式定义查询字段
                        $array =  explode('|',$key);
                        $str   =  array();
                        foreach ($array as $m=>$k){
                            $v =  $multi?$val[$m]:$val;
                            $str[]   = '('.$this->parseWhereItem($this->parseKey($k),$v).')';
                        }
                        $whereStr .= implode(' OR ',$str);
                    }elseif(strpos($key,'&')){
                        $array =  explode('&',$key);
                        $str   =  array();
                        foreach ($array as $m=>$k){
                            $v =  $multi?$val[$m]:$val;
                            $str[]   = '('.$this->parseWhereItem($this->parseKey($k),$v).')';
                        }
                        $whereStr .= implode(' AND ',$str);
                    }else{
                        $whereStr .= $this->parseWhereItem($this->parseKey($key),$val);
                    }
                }
                $whereStr .= ' )'.$operate;
            }
            $whereStr = substr($whereStr,0,-strlen($operate));
        }
        return empty($whereStr)?'':' WHERE '.$whereStr;
    }

    /**
     * where子单元分析
     * @param $key
     * @param $val
     * @return string
     * @code
     * in 的特殊用法.
     * $sqlBuilderOptions['where'] = array('catid' => array('in',$tmpArr));
     */
    private function parseWhereItem($key,$val)
    {
        $whereStr = '';
        if(is_array($val)) {
            if(is_string($val[0])) {
                if(preg_match('/^(EQ|NEQ|GT|EGT|LT|ELT)$/i',$val[0])) { // 比较运算
                    $whereStr .= $key.' '.$this->_comparison[strtolower($val[0])].' '.$this->parseValue($val[1]);
                }elseif(preg_match('/^(NOTLIKE|LIKE)$/i',$val[0])){// 模糊查找
                    if(is_array($val[1])) {
                        $likeLogic  =   isset($val[2])?strtoupper($val[2]):'OR';
                        $likeStr    =   $this->_comparison[strtolower($val[0])];
                        $like       =   array();
                        foreach ($val[1] as $item){
                            $like[] = $key.' '.$likeStr.' '.$this->parseValue($item);
                        }
                        $whereStr .= '('.implode(' '.$likeLogic.' ',$like).')';
                    }else{
                        $whereStr .= $key.' '.$this->_comparison[strtolower($val[0])].' '.$this->parseValue($val[1]);
                    }
                }elseif('exp'==strtolower($val[0])){ // 使用表达式
                    $whereStr .= ' ('.$key.' '.$val[1].') ';
                }elseif(preg_match('/IN/i',$val[0])){ // IN 运算
                    if(isset($val[2]) && 'exp'==$val[2]) {
                        $whereStr .= $key.' '.strtoupper($val[0]).' '.$val[1];
                    }else{
                        if(is_string($val[1])) {
                            $val[1] =  explode(',',$val[1]);
                        }
                        $zone      =   implode(',',$this->parseValue($val[1]));
                        $whereStr .= $key.' '.strtoupper($val[0]).' ('.$zone.')';
                    }
                }elseif(preg_match('/BETWEEN/i',$val[0])){ // BETWEEN运算
                    $data = is_string($val[1])? explode(',',$val[1]):$val[1];
                    $whereStr .=  ' ('.$key.' '.strtoupper($val[0]).' '.$this->parseValue($data[0]).' AND '.$this->parseValue($data[1]).' )';
                }else{
                    $this->_handleError('_EXPRESS_ERROR_:'.$val[0]);
                }
            }else {
                $count = count($val);
                if(in_array(strtoupper(trim($val[$count-1])),array('AND','OR','XOR'))) {
                    $rule   = strtoupper(trim($val[$count-1]));
                    $count  = $count -1;
                }else{
                    $rule   = 'AND';
                }
                for($i=0;$i<$count;$i++) {
                    $data = is_array($val[$i])?$val[$i][1]:$val[$i];
                    if('exp'==strtolower($val[$i][0])) {
                        $whereStr .= '('.$key.' '.$data.') '.$rule.' ';
                    }else{
                        $op = is_array($val[$i])?$this->_comparison[strtolower($val[$i][0])]:'=';
                        $whereStr .= '('.$key.' '.$op.' '.$this->parseValue($data).') '.$rule.' ';
                    }
                }
                $whereStr = substr($whereStr,0,-4);
            }
        }else {
            //对字符串类型字段采用模糊匹配
            if($this->_dbLikeFields && preg_match('/('.$this->_dbLikeFields.')/i',$key)) {
                $val  =  '%'.$val.'%';
                $whereStr .= $key.' LIKE '.$this->parseValue($val);
            }else {
                $whereStr .= $key.' = '.$this->parseValue($val);
            }
        }
        return $whereStr;
    }

    /**
     * 特殊条件分析
     * @param string $key
     * @param mixed $val
     * @return string
     */
    private function parseThinkWhere($key,$val) {
        $whereStr   = '';
        switch($key) {
            case '_string':
                // 字符串模式查询条件
                $whereStr = $val;
                break;
            case '_complex':
                // 复合查询条件
                $whereStr = substr($this->parseWhere($val),6);
                break;
            case '_query':
                // 字符串模式查询条件
                parse_str($val,$where);
                if(isset($where['_logic'])) {
                    $op   =  ' '.strtoupper($where['_logic']).' ';
                    unset($where['_logic']);
                }else{
                    $op   =  ' AND ';
                }
                $array   =  array();
                foreach ($where as $field=>$data)
                    $array[] = $this->parseKey($field).' = '.$this->parseValue($data);
                $whereStr   = implode($op,$array);
                break;
        }
        return $whereStr;
    }

    /**
     * @param $limit
     * @return string
     */
    private function parseLimit($limit) {
        return !empty($limit)?   ' LIMIT '.$limit.' ':'';
    }

    /**
     * join分析
     * @param mixed $join
     * @return string
     */
    private function parseJoin($join) {
        $joinStr = '';
        if(!empty($join)) {
            if(is_array($join)) {
                foreach ($join as $_join){
                    if(false !== stripos($_join,'JOIN'))
                        $joinStr .= ' '.$_join;
                    else
                        $joinStr .= ' LEFT JOIN ' .$_join;
                }
            }else{
                $joinStr .= ' LEFT JOIN ' .$join;
            }
        }
        //将__TABLE_NAME__这样的字符串替换成正规的表名,并且带上前缀和后缀
        $joinStr = preg_replace("/__([A-Z_-]+)__/esU",$this->_tbPre.".strtolower('$1')",$joinStr);
        return $joinStr;
    }

    /**
     * order分析
     * @param mixed $order
     * @return string
     */
    private function parseOrder($order) {
        if(is_array($order)) {
            $array   =  array();
            foreach ($order as $key=>$val){
                if(is_numeric($key)) {
                    $array[] =  $this->parseKey($val);
                }else{
                    $array[] =  $this->parseKey($key).' '.$val;
                }
            }
            $order   =  implode(',',$array);
        }
        return !empty($order)?  ' ORDER BY '.$order:'';
    }

    /**
     * group分析
     * @param mixed $group
     * @return string
     */
    private function parseGroup($group) {
        return !empty($group)? ' GROUP BY '.$group:'';
    }

    /**
     * having分析
     * @param string $having
     * @return string
     */
    private function parseHaving($having) {
        return  !empty($having)?   ' HAVING '.$having:'';
    }

    /**
     * distinct分析
     * @param mixed $distinct
     * @return string
     */
    private function parseDistinct($distinct) {
        return !empty($distinct)?   ' DISTINCT ' :'';
    }

    /**
     * union分析
     * @param mixed $union
     * @return string
     */
    private function parseUnion($union) {
        $sql = array();
        if(empty($union)) return '';
        if(isset($union['_all'])) {
            $str  =   'UNION ALL ';
            unset($union['_all']);
        }else{
            $str  =   'UNION ';
        }
        foreach ($union as $u){
            $sql[] = $str.(is_array($u)?$this->buildSelect($u):$u);
        }
        return implode(' ',$sql);
    }

    /**
     * 插入记录
     * @access public
     * @param mixed $data 数据
     * @param array $options 参数表达式
     * @param boolean $replace 是否replace
     * @return string | integer
     */
    public function buildInsert($data,$options=array(),$replace=false) {
        $values  =  $fields    = array();
        foreach ($data as $key=>$val){
            $value   =  $this->parseValue($val);
            if(is_scalar($value)) { // 过滤非标量数据
                $values[]   =  $value;
                $fields[]   =  $this->parseKey($key);
            }
        }
        $sql   =  ($replace?'REPLACE':'INSERT').' INTO '.$this->parseTable($options['table']).' ('.implode(',', $fields).') VALUES ('.implode(',', $values).')';
        return $sql;
    }


    /**
     * 更新记录
     * @access public
     * @param mixed $data 数据
     * @param array $options 表达式
     * @return string | integer
     */
    public function buildUpdate($data,$options) {
        $sql   = 'UPDATE '
            .$this->parseTable($options['table'])
            .$this->parseSet($data)
            .$this->parseWhere(isset($options['where'])?$options['where']:'')
            .$this->parseOrder(isset($options['order'])?$options['order']:'')
            .$this->parseLimit(isset($options['limit'])?$options['limit']:'');
        return $sql;
    }

    /**
     * 删除记录
     * @access public
     * @param array $options 表达式
     * @return string | integer
     */
    public function buildDelete($options=array()) {
        $sql   = 'DELETE FROM '
            .$this->parseTable($options['table'])
            .$this->parseWhere(isset($options['where'])?$options['where']:'')
            .$this->parseOrder(isset($options['order'])?$options['order']:'')
            .$this->parseLimit(isset($options['limit'])?$options['limit']:'');
        return $sql;
    }


    /**
     * 生成查询SQL
     * @access public
     * @param array $options 表达式
     * @return string
     */
    public function buildSelect($options=array()) {
        $sql  =   $this->parseSql($this->_selectSql,$options);
        return $sql;
    }

    /**
     * 替换SQL语句中表达式
     * @param $sql
     * @param array $options
     * @return mixed
     */
    private function parseSql($sql,$options=array()){
        if (!$options) {
            $options = $this->_options;
        }
        $sql   = str_replace(
            array('%TABLE%','%DISTINCT%','%FIELD%','%JOIN%','%WHERE%','%GROUP%','%HAVING%','%ORDER%','%LIMIT%','%UNION%'),
            array(
                $this->parseTable($options['table']),
                $this->parseDistinct(isset($options['distinct'])?$options['distinct']:false),
                $this->parseField(isset($options['field'])?$options['field']:'*'),
                $this->parseJoin(isset($options['join'])?$options['join']:''),
                $this->parseWhere(isset($options['where'])?$options['where']:''),
                $this->parseGroup(isset($options['group'])?$options['group']:''),
                $this->parseHaving(isset($options['having'])?$options['having']:''),
                $this->parseOrder(isset($options['order'])?$options['order']:''),
                $this->parseLimit(isset($options['limit'])?$options['limit']:''),
                $this->parseUnion(isset($options['union'])?$options['union']:'')
            ),$sql);
        return $sql;
    }

    /**
     * SQL指令安全过滤
     * @access public
     * @param string $str  SQL字符串
     * @return string
     */
    private function escapeString($str) {
        return addslashes($str);
    }

    /**
     * 清空选项
     */
    public function emptyOpts()
    {
        $this->_options = array();
        return $this;
    }

    /**
     * @param $table
     * @return SQLBuilder
     */
    public function table($table)
    {
        $this->_options['table'] = $table;

        return $this;
    }

    /**
     * @param $where
     * @return SQLBuilder
     */
    public function where($where)
    {
        $this->_options['where'] = $where;

        return $this;
    }

    /**
     * @param $distinct
     * @return SQLBuilder
     */
    public function distinct($distinct)
    {
        $this->_options['distinct'] = $distinct;

        return $this;
    }

    /**
     * @param $field
     * @return SQLBuilder
     */
    public function field($field)
    {
        $this->_options['field'] = $field;

        return $this;
    }

    /**
     * @param $join
     * @return SQLBuilder
     */
    public function join($join)
    {
        $this->_options['join'] = $join;

        return $this;
    }

    /**
     * @param $group
     * @return SQLBuilder
     */
    public function group($group)
    {
        $this->_options['group'] = $group;

        return $this;
    }

    /**
     * @param $having
     * @return SQLBuilder
     */
    public function having($having)
    {
        $this->_options['having'] = $having;

        return $this;
    }

    /**
     * @param $order
     * @return SQLBuilder
     */
    public function order($order)
    {
        $this->_options['order'] = $order;

        return $this;
    }

    /**
     * @param $limit
     * @return SQLBuilder
     */
    public function limit($limit)
    {
        $this->_options['limit'] = $limit;

        return $this;
    }

    /**
     * @param $union
     * @return SQLBuilder
     */
    public function union($union)
    {
        $this->_options['union'] = $union;

        return $this;
    }

    /**
     * @param $msg 错误处理
     */
    private function _handleError($msg)
    {
        die($msg);
    }
}
